Repository navigation
Conversation
`import X = A.B` became `const X = A.B` and nothing recorded what X names. A read through X was never replaced by a value, so an enum initializer `P = X.Q` was no constant and the next member got `undefined`. The parse pass now records each entity alias. Between the parse and the visit passes, bind_ts_import_equals_aliases gives the alias the members of the enum or namespace of this file that it names, as a second key of ref_to_ts_namespace_member. The readers of that table do not change. The alias statement stays printed: a fold through the alias keeps its use.
The build for --aot asks the checker which enum reads the transpiler replaces by values. resolve_member_path refused every first name that is an alias. It now follows an `import X = A.B` alias as the transpiler binds it: an entity alias that no namespace exports, whose first name is an enum or a namespace of the file, another part's export of it, or another such alias. The printer also puts a member of an enum of another module in parentheses left of `**` when its value is printed there: a member that the alias fold made a constant can be a negative number. Tests: transpiler.test.js (38 new, 23 of them fail on main, 9 todo rows hold tsc's values), ts/ImportEqualsEnumAlias, the fixture enum-access-import-equals of test/aot/checker-queries, and the program test/aot/parallel/enums/enum-read-through-import-equals-alias.ts.
An alias that names an alias written before it finds that one bound, so the index of the aliases is built only for a chain that is written leaf first. One buffer holds the names of every target.
Collaborator
Author
Collaborator
Author
|
Status
|
The scope walk reads only its arguments. As a method of the parser it was compiled once per parser instantiation.
This was referenced Nov 2, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Parked: it needs a maintainer's answer.
Per-file tsc gives
3 4on the seven reported shapes. esbuild and bun give3 undefined. No user reported them. May the transpiler differ from esbuild for enum values?Y["R"] = Y["P"] + 1) ins_enum(src/js_parser/visit/visit_stmt.rs:2417), like oxc and sucrase.it.todorows and this PR closes.Problem
enum Z { Q = 3 } import X = Z; enum Y { P = X.Q, R }emitsY[Y["R"] = undefined] = "R".import X = Zbecomesconst X = Z(src/js_parser/parse/parse_typescript.rs:557) and nothing records whatXnames, soRgets no number.Fix
bind_ts_import_equals_aliasesmakes each alias of an enum or namespace of the file a key ofref_to_ts_namespace_member.--aotchecker follows the same aliases. The printer parenthesizes a negative imported member left of**.test/bundler/transpiler/transpiler.test.js(23 new tests fail on main).Background
ref_to_ts_namespace_membermaps a symbol to the members of its enum or namespace.handle_identifierreads it to replaceZ.Qby its value.s_enumand a lazy alias entry, not option 2.Downsides
bun runcan drop the alias statement, it disagrees with--aoton two shapes, and an importer's write to a folded member does not parse.undefined..js) or +3 (.ts), each alias +279 to +599.Pgrows 8 bytes,.text9,216.Notes
Self-review. A review of this diff said not to merge it as it is, and to ask the question above first. I checked each reason below.
void 0for all seven shapes.ts.transpileModule6.0.2 prints3 4for all seven shapes, so option 1 follows that reference. The same issue refuses option 2 for a read of another file.elseofs_enum(src/js_parser/visit/visit_stmt.rs:2417), and this PR changes nothing there.const a = 3; enum Y { P = a, R }(the kind of initializer in Constant folding is not working insideconst enum#19581) reaches the same arm and prints3 undefinedon main and on this branch. For the smallest shape, oxc-transform 0.156.0 printsY["R"] = 1 + Y["P"]and sucrase 3.35.1 printsconst R = P + 1.import.meta.mainnode and the left operand of**by level #33447 and js_printer: inline a cross-module enum member only where the parser counted a read #42454 change the printer for a value that is inlined.**printer fix alone (js_printer: parenthesize inlined negative enum value on LHS of ** #36133 or js_printer: parenthesize the foldedimport.meta.mainnode and the left operand of**by level #33447), then js_parser: register the closure argument of a TypeScript namespace block #46938, then this PR rebased with no printer lines.Defects the review found, not fixed yet. Checked on release builds of main 3f3a9ff and of 57d7b77.
enum Z { Q = 3 },import X = Z;,X.Q;andconsole.log(eval("X.Q"))print3on main. This branch throwsReferenceError: X is not definedunderbun run: the folded statement is removed, its part gives the use back, and the alias has no use left.bun build --no-bundlekeeps the statement.bun runand a--aotexecutable disagree on two shapes where main agrees.namespace M { export function F() {} export namespace F { export enum E { A = 1 } } } import X = M.F.E; namespace N { export enum Y { P = X.A, R } }prints1 undefinedunderbun runand1 2in the executable. Main prints1 undefinedin both.namespace Outer { export namespace N { export enum Z { Q = 3 } } import A = N.Z; export namespace Later { export enum W { P = A.Q, R } } }prints3 4underbun runand3 undefinedin the executable. Main prints3 undefinedin both.export enum Y { P = X.Q, R }in one file,(Y as any).P = 14in an importer bundles toY.P = 14on main and to3 /* P */ = 14here, which does not parse. Main prints the same text when the member isP = Z.Q. js_printer: inline a cross-module enum member only where the parser counted a read #42454 changes that printer path.find_symbol_with_record_usage::<false>in place of the copy infind_ts_name_binding. Bind in one pass in source order, or pin the cycle guard with tests (import A = B; import B = A). Add tests withtreeShakingandminify.syntax, and forexports.replace. Useit.failingrows in place of the rows that pinR: undefined.Corrections to earlier text of this description.
createProgram.ts.transpileModule(one file at a time) gives the same3 4for the seven shapes.import W = X; import X = A.Z) still throws underbun runand--no-bundle. Underbun buildit now prints3 4, because the bundler drops the unusedconst W = X. No test pins that. js_parser: remove unused TS import-equals before the import scan, and scan each Part once #36143 does not change how the statement is printed.export import X = ZkeepsX.Qas a run-time read, as on main.constor arequireimport binds it (3,412 to 4,011).Where it comes from. A fuzzer found the shapes. No user reported them. The values below are from
Bun.Transpileroutput run withnew Function, and tsc 6.0.2 throughcreateProgram.enum Z { Q = 3 } import X = Z; enum Y { P = X.Q, R }3 undefined3 43 43 undefined3 43 43 undefined3 43 43 undefined3 43 4import X = A.Z), read by a top-level enum3 undefined3 undefined3 4import N = A,N.Z.Q), read by a top-level enum3 undefined3 undefined3 43 undefined3 undefined3 43 undefined3 43 4ReferenceError3 43 4The three rows that stay are the visit order: bun visits a top-level enum before the body of
namespace A, so the members ofA.Zhave no values yet. The direct nameenum Y { P = A.Z.Q, R }gives3 undefinedon main for the same reason. #42118 changes that order (see the corrections above). Threeit.todorows hold tsc's values.What else folds.
X['Q'],X.Q + 1,-X.Q, a string member (no reverse key, as tsc), a merged enum, an alias of a namespace or of a nested namespace, a module-levelexport import X = Z, the inner of two aliases of one name, and a read outside an enum (const x = X.Q). A bundle that importsYgets its members as numbers (ts/ImportEqualsEnumAlias).Not in this change. Each line prints as on main.
it.todorows hold tsc's values.export import X = Zinside a namespace: the bare read in its block, the bare read from a sibling block,M.X.Qfrom outside, andimport W = M.X. The bare name of such an export has no local binding to print.import X = Z.Q; enum Y { P = X, R }.import W = X; import X = A.Z). The enum folds, but the keptconst W = Xstill throws at run time underbun runand--no-bundle, as on main (see the corrections above). tsc printsvar.function f(X) { enum Y { P = X.Q, R } }). tsc folds past the parameter. bun binds the parameter, on purpose.X?.Qdoes not fold, likeZ?.Q.The
--aotchecker.resolve_member_path(src/sema/check/aot/enum_constants.rs) says which enum reads the transpiler replaces by values. It refused every first name that is an alias.is_first_name_followednow follows an entity alias that no namespace exports, whose first name is an enum or a namespace of the file, an export of another part of its namespace, or another such alias. A probe compares 25 alias reads in the transpiler's output with the checker's answers: 24 agree. The last one is an alias through a namespace that shares a block with a function of its name (import X = M.F): the checker follows it and the transpiler does not, as for the direct pathM.F.E.Aon main. #46938 has the rule for that shape.Overlaps.
enum_constants.rs, and the same place innarrowing.programs. The second of the two to land rebases.Propertyunder a symbol ref in the namespace export loop #47001: storesPropertyin the namespace export loop. No line in common.**printer arm. The lines for a member of another module are here because this fold can make such a member a negative constant: without themY.P ** 2in an importing module prints-3 /* P */ ** 2, a SyntaxError, where main printed 9.const, see above.import.meta.mainnode and the left operand of**by level #33447: other entrances to the same arm ofs_enumand other edits of the printer arms for an inlined value.parse_type_script_import_equals_stmt.Measurements. Release builds of main 3f3a9ff and of 57d7b77 in one container. The commit after 57d7b77 only turns the method
find_ts_name_bindinginto a free function. Instructions are user-mode counts.size_of::<P>()ParserSnapshotP::init, one-statement.jsand.ts(gdb single-step)_parseof a one-statement.js(gdb single-step)_parseof a one-statement.ts(gdb single-step)maybe_rewrite_property_access_for_namespace(gdb single-step)constin place of the aliasimport F = Baz.Barthat names nothing of the file (slope, 1,000 to 4,000).textnm -S):handle_identifier,find_symbol_with_record_usage,maybe_rewrite_property_access,maybe_rewrite_property_access_for_namespace,s_enumandvisit_stmtsare identical in every instantiation. Newbind_ts_import_equals_aliases4,591 (one copy).P::init+10 in each of four copies.parse_type_script_import_equals_stmt+111 and +97.parse_type_script_namespace_stmt+26 in two copies._parse::<true>+33.print_expr-338 to +465 in five copies, andprints_as_imported_enum_value191 in five copies. Checker:is_first_name_followed3,131,resolve_member_path+64,is_declared_around(457) inlined.bun build --no-bundlefromperf_event_open, minimum of 5 runs, spread under 700. The chain of 100,000 aliases written leaf first resolves (console.log(3 /* Q */)) with no recursion..ts,.tsx,.mtsand.ctsfiles under sixBun.Transpileroption sets (default,inlinewith target bun,minify.syntax, all ofminify,treeShakingwithtrimUnusedImports,deadCodeEliminationoff). The 24 are four programs oftest/aot/parallel/enumsunder each set: three read an enum through an alias, and one has a negative member left of**. The alias statements of those files are printed as before (2 and 7).--aotparity:test/aot/run-parallel.tswith the release build passes 24 of 24 programs oftest/aot/parallel/enums, compiled and with--aot-erase-types.enums-namespaces-read-paths-brackets.tsreads 8 members through aliases on 3 lines: plain Bun and the executable now both print the values.3 4where main printed3 undefined. The second run reads it.Tests.
transpiler.test.js: 38 new tests and 9it.todorows. 23 of the 38 fail on main, and 15 pin what must not change.ts/ImportEqualsEnumAliasfails on main.checker-queries-narrowing.test.ts: 38 pass, one new fixture (enum-access-import-equals). New programtest/aot/parallel/enums/enum-read-through-import-equals-alias.ts.Also ran (debug build, all pass): the rest of
transpiler.test.jsandesbuild/ts.test.ts,esbuild/{importstar_ts,dce},bundler_edgecase,transpiler/runtime-transpiler,cli/run/transpiler-cache.[human-review] gate passed · iteration 1 · 11 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 0 rejected · iteration 1
evidence per changed file